// Copyright 2016 The Fuchsia Authors
// Copyright (c) 2008-2010, 2015 Travis Geiselbrecht
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT

/**
 * @file
 * @brief  Manage graphics console
 *
 * This file contains functions to provide stdout to the graphics console.
 *
 * @ingroup graphics
 */

#include <assert.h>
#include <debug.h>
#include <lib/cmdline.h>
#include <lib/gfx.h>
#include <lib/gfxconsole.h>
#include <lib/io.h>
#include <stdlib.h>
#include <string.h>

#include <dev/display.h>

#define TEXT_COLOR 0xffffffff
#define BACK_COLOR 0xff000000

#define CRASH_TEXT_COLOR 0xffffffff
#define CRASH_BACK_COLOR 0xffe000e0

/** @addtogroup graphics
 * @{
 */

/**
 * @brief  Represent state of graphics console
 */
static struct {
  // main surface to draw on
  gfx_surface* surface;
  // underlying hw surface, if different from above
  gfx_surface* hw_surface;

  // surface to do single line sub-region flushing with
  gfx_surface line;
  uint linestride;

  uint rows, columns;
  uint extray;  // extra pixels left over if the rows doesn't fit precisely

  uint x, y;

  uint32_t front_color;
  uint32_t back_color;
} gfxconsole;

static void draw_char(char c, const struct gfx_font* font) {
  gfx_putchar(gfxconsole.surface, font, c, gfxconsole.x * font->width, gfxconsole.y * font->height,
              gfxconsole.front_color, gfxconsole.back_color);
}

void gfxconsole_putpixel(unsigned x, unsigned y, unsigned color) {
  gfx_putpixel(gfxconsole.surface, x, y, color);
}

static const struct gfx_font* font = &gfx_font_9x16;

static bool gfxconsole_putc(char c) {
  static enum { NORMAL, ESCAPE } state = NORMAL;
  static uint32_t p_num = 0;
  bool inval = 0;

  if (state == NORMAL) {
    switch (c) {
      case '\r':
        gfxconsole.x = 0;
        break;
      case '\n':
        gfxconsole.y++;
        inval = 1;
        break;
      case '\b':
        // back up one character unless we're at the left side
        if (gfxconsole.x > 0) {
          gfxconsole.x--;
        }
        break;
      case '\t':
        gfxconsole.x = ROUNDUP(gfxconsole.x + 1, 8);
        break;
      case 0x1b:
        p_num = 0;
        state = ESCAPE;
        break;
      default:
        draw_char(c, font);
        gfxconsole.x++;
        break;
    }
  } else if (state == ESCAPE) {
    if (c >= '0' && c <= '9') {
      p_num = (p_num * 10) + (c - '0');
    } else if (c == 'D') {
      if (p_num <= gfxconsole.x)
        gfxconsole.x -= p_num;
      state = NORMAL;
    } else if (c == '[') {
      // eat this character
    } else {
      draw_char(c, font);
      gfxconsole.x++;
      state = NORMAL;
    }
  }

  if (gfxconsole.x >= gfxconsole.columns) {
    gfxconsole.x = 0;
    gfxconsole.y++;
    inval = 1;
  }
  if (gfxconsole.y >= gfxconsole.rows) {
    // scroll up
    gfx_copyrect(gfxconsole.surface, 0, font->height, gfxconsole.surface->width,
                 gfxconsole.surface->height - font->height - gfxconsole.extray, 0, 0);
    gfxconsole.y--;

    // clear the bottom line
    gfx_fillrect(gfxconsole.surface, 0,
                 gfxconsole.surface->height - font->height - gfxconsole.extray,
                 gfxconsole.surface->width, font->height, gfxconsole.back_color);
    gfx_flush(gfxconsole.surface);
    inval = 1;
  }
  return inval;
}

static void gfxconsole_print_callback(print_callback_t* cb, const char* str, size_t len) {
  int refresh_full_screen = 0;
  for (size_t i = 0; i < len; i++) {
    if (str[i] == '\n')
      gfxconsole_putc('\r');
    refresh_full_screen |= gfxconsole_putc(str[i]);
  }

  // blit from the software surface to the hardware
  if (gfxconsole.surface != gfxconsole.hw_surface) {
    if (refresh_full_screen) {
      gfx_surface_blend(gfxconsole.hw_surface, gfxconsole.surface, 0, 0);
    } else {
      // Only re-blit the active console line.
      // Since blend only works in whole surfaces, configure a sub-surface
      // to use as the blend source.
      gfxconsole.line.ptr =
          ((uint8_t*)gfxconsole.surface->ptr) + (gfxconsole.y * gfxconsole.linestride);
      gfx_surface_blend(gfxconsole.hw_surface, &gfxconsole.line, 0, gfxconsole.y * font->height);
    }
    gfx_flush(gfxconsole.hw_surface);
  } else {
    gfx_flush(gfxconsole.surface);
  }
}

static print_callback_t cb = {.entry = {}, .print = gfxconsole_print_callback, .context = NULL};

static void gfxconsole_setup(gfx_surface* surface, gfx_surface* hw_surface) {
  const char* fname = gCmdline.GetString("gfxconsole.font");
  if (fname != NULL) {
    if (!strcmp(fname, "18x32")) {
      font = &gfx_font_18x32;
    } else if (!strcmp(fname, "9x16")) {
      font = &gfx_font_9x16;
    }
  }
  // set up the surface
  gfxconsole.surface = surface;
  gfxconsole.hw_surface = hw_surface;

  // set up line-height sub-surface, for line-only invalidation
  memcpy(&gfxconsole.line, surface, sizeof(*surface));
  gfxconsole.line.height = font->height;
  gfxconsole.linestride = surface->stride * surface->pixelsize * font->height;

  // calculate how many rows/columns we have
  gfxconsole.rows = surface->height / font->height;
  gfxconsole.columns = surface->width / font->width;
  gfxconsole.extray = surface->height - (gfxconsole.rows * font->height);

  dprintf(SPEW, "gfxconsole: rows %u, columns %u, extray %u\n", gfxconsole.rows, gfxconsole.columns,
          gfxconsole.extray);
}

static void gfxconsole_clear(bool crash_console) {
  // start in the upper left
  gfxconsole.x = 0;
  gfxconsole.y = 0;

  if (crash_console) {
    gfxconsole.front_color = CRASH_TEXT_COLOR;
    gfxconsole.back_color = CRASH_BACK_COLOR;
  } else {
    gfxconsole.front_color = TEXT_COLOR;
    gfxconsole.back_color = BACK_COLOR;
  }

  // fill screen with back color
  gfx_fillrect(gfxconsole.surface, 0, 0, gfxconsole.surface->width, gfxconsole.surface->height,
               gfxconsole.back_color);
  gfx_flush(gfxconsole.surface);
}

/**
 * @brief  Initialize graphics console on given drawing surface.
 *
 * The graphics console subsystem is initialized, and registered as
 * an output device for debug output.
 */
void gfxconsole_start(gfx_surface* surface, gfx_surface* hw_surface) {
  DEBUG_ASSERT(gfxconsole.surface == NULL);

  gfxconsole_setup(surface, hw_surface);
  gfxconsole_clear(false);

  // register for debug callbacks
  register_print_callback(&cb);
}

static gfx_surface hw_surface;
static gfx_surface sw_surface;
static struct display_info dispinfo;

zx_status_t gfxconsole_display_get_info(struct display_info* info) {
  if (gfxconsole.surface) {
    memcpy(info, &dispinfo, sizeof(*info));
    return 0;
  } else {
    return -1;
  }
}

/**
 * @brief  Initialize graphics console and bind to a display
 *
 * If the display was previously initialized, first it is shut down and
 * detached from the print callback.
 *
 * If the new display_info is NULL, nothing else is done, otherwise the
 * display is initialized against the provided display_info.
 *
 * If raw_sw_fb is non-NULL it is a memory large enough to be a backing
 * surface (stride * height * pixelsize) for the provided hardware display.
 * This is used for very early framebuffer init before the heap is alive.
 */
void gfxconsole_bind_display(struct display_info* info, void* raw_sw_fb) {
  static bool active = false;
  bool same_as_before = false;
  struct gfx_surface hw;

  if (active) {
    // on re-init or detach, we need to unhook from print callbacks
    active = false;
    unregister_print_callback(&cb);
  }
  if (info == NULL) {
    return;
  }

  if (gfx_init_surface_from_display(&hw, info)) {
    return;
  }
  if (info->flags & DISPLAY_FLAG_CRASH_FRAMEBUFFER) {
    // "bluescreen" path. no allocations allowed
    memcpy(&hw_surface, &hw, sizeof(hw));
    gfxconsole_setup(&hw_surface, &hw_surface);
    memcpy(&dispinfo, info, sizeof(*info));
    gfxconsole_clear(true);
    register_print_callback(&cb);
    active = true;
    return;
  }
  if ((hw.format == hw_surface.format) && (hw.width == hw_surface.width) &&
      (hw.height == hw_surface.height) && (hw.stride == hw_surface.stride) &&
      (hw.pixelsize == hw_surface.pixelsize)) {
    // we are binding to a new hw surface with the same properties
    // as the existing one
    same_as_before = true;
  } else {
    // we cannot re-use the sw backing surface, so destroy it
    if (sw_surface.ptr && (sw_surface.flags & GFX_FLAG_FREE_ON_DESTROY)) {
      free(sw_surface.ptr);
    }
    memset(&sw_surface, 0, sizeof(sw_surface));
  }
  memcpy(&hw_surface, &hw, sizeof(hw));

  gfx_surface* s = &hw_surface;
  if (info->flags & DISPLAY_FLAG_HW_FRAMEBUFFER) {
    if (!same_as_before) {
      // we can't re-use the existing sw_surface, create a new one
      if (gfx_init_surface(&sw_surface, raw_sw_fb, hw_surface.width, hw_surface.height,
                           hw_surface.stride, hw_surface.format, 0)) {
        return;
      }
    }
    s = &sw_surface;
  } else {
    // for non-hw surfaces we're not using a backing surface
    // so we can't be the same as before and must fully init
    same_as_before = false;
  }

  gfxconsole_setup(s, &hw_surface);

  if (!same_as_before) {
    // on first init, or different-backing-buffer re-init
    // we clear and reset to x,y @ 0,0
    gfxconsole_clear(false);
  }

  memcpy(&dispinfo, info, sizeof(*info));
  register_print_callback(&cb);
  active = true;
}

void gfxconsole_flush() {
  if (gfxconsole.surface != gfxconsole.hw_surface) {
    gfx_surface_blend(gfxconsole.hw_surface, gfxconsole.surface, 0, 0);
    gfx_flush(gfxconsole.hw_surface);
  } else {
    gfx_flush(gfxconsole.surface);
  }
}
